// Copyright (C) 2008 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.dotspots.rpcplus.client.codec.impl;

import com.dotspots.rpcplus.client.codec.JsonDecoder;
import com.dotspots.rpcplus.client.codec.JsonParseException;
import com.google.gwt.core.client.JavaScriptObject;

/**
 * Decodes proper JSON using a fast tokenizer.
 */
public class FastJsonDecoder implements JsonDecoder {
	JavaScriptObject decoder = getDecoder();

	public JavaScriptObject decode(String json) throws JsonParseException {
		try {
			return decode(decoder, json);
		} catch (Throwable t) {
			throw new JsonParseException(t);
		}
	}

	/**
	 * Parses a string of well-formed JSON text.
	 * 
	 * If the input is not well-formed, then behavior is undefined, but it is deterministic and is guaranteed not to
	 * modify any object other than its return value.
	 * 
	 * This does not use `eval` so is less likely to have obscure security bugs than json2.js. It is optimized for
	 * speed, so is much faster than json_parse.js.
	 * 
	 * This library should be used whenever security is a concern (when JSON may come from an untrusted source), speed
	 * is a concern, and erroring on malformed JSON is *not* a concern.
	 * 
	 * Pros Cons +-----------------------+-----------------------+ json_sans_eval.js | Fast, secure | Not validating |
	 * +-----------------------+-----------------------+ json_parse.js | Validating, secure | Slow |
	 * +-----------------------+-----------------------+ json2.js | Fast, some validation | Potentially insecure |
	 * +-----------------------+-----------------------+
	 * 
	 * json2.js is very fast, but potentially insecure since it calls `eval` to parse JSON data, so an attacker might be
	 * able to supply strange JS that looks like JSON, but that executes arbitrary javascript. If you do have to use
	 * json2.js with untrusted data, make sure you keep your version of json2.js up to date so that you get patches as
	 * they're released.
	 * 
	 * @param {string} json per RFC 4627
	 * @param {function} opt_reviver optional function that reworks JSON objects post-parse per Chapter 15.12 of
	 *        EcmaScript3.1. If supplied, the function is called with a string key, and a value. The value is the
	 *        property of 'this'. The reviver should return the value to use in its place. So if dates were serialized
	 *        as {@code "type": "Date", "time": 1234 } , then a reviver might look like {@code function (key, value) if
	 *        (value && typeof value === 'object' && 'Date' === value.type) return new Date(value.time); } else { return
	 *        value; } }}. If the reviver returns {@code undefined} then the property named by key will be deleted from
	 *        its container. {@code this} is bound to the object containing the specified property.
	 * @return {Object|Array}
	 * @author Mike Samuel <mikesamuel@gmail.com>
	 */
	public static native JavaScriptObject getDecoder() /*-{
		var number
		    = '(?:-?\\b(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\\b)';
		var oneChar = '(?:[^\\0-\\x08\\x0a-\\x1f\"\\\\]'
		    + '|\\\\(?:[\"/\\\\bfnrt]|u[0-9A-Fa-f]{4}))';
		var string = '(?:\"' + oneChar + '*\")';

		// Will match a value in a well-formed JSON file.
		// If the input is not well-formed, may match strangely, but not in an unsafe
		// way.
		// Since this only matches value tokens, it does not match whitespace, colons,
		// or commas.
		var jsonToken = new RegExp(
		    '(?:false|true|null|[\\{\\}\\[\\]]'
		    + '|' + number
		    + '|' + string
		    + ')', 'g');

		// Matches escape sequences in a string literal
		var escapeSequence = new RegExp('\\\\(?:([^u])|u(.{4}))', 'g');

		// Decodes escape sequences in object literals
		var escapes = {
		  '"': '"',
		  '/': '/',
		  '\\': '\\',
		  'b': '\b',
		  'f': '\f',
		  'n': '\n',
		  'r': '\r',
		  't': '\t'
		};
		function unescapeOne(_, ch, hex) {
		  return ch ? escapes[ch] : String.fromCharCode(parseInt(hex, 16));
		}

		// A non-falsy value that coerces to the empty string when used as a key.
		var EMPTY_STRING = new String('');
		var SLASH = '\\';

		// Constructor to use based on an open token.
		var firstTokenCtors = { '{': Object, '[': Array };

		var hop = Object.hasOwnProperty;

		return function (json) {
		  // Split into tokens
		  var toks = json.match(jsonToken);
		  // Construct the object to return
		  var result;
		  var tok = toks[0];
		  if ('{' === tok) {
		    result = {};
		  } else if ('[' === tok) {
		    result = [];
		  } else {
		    throw new Error(tok);
		  }

		  // If undefined, the key in an object key/value record to use for the next
		  // value parsed.
		  var key;
		  // Loop over remaining tokens maintaining a stack of uncompleted objects and
		  // arrays.
		  var stack = [result];
		  for (var i = 1, n = toks.length; i < n; ++i) {
		    tok = toks[i];

		    var cont;
		    switch (tok.charCodeAt(0)) {
		      default:  // sign or digit
		        cont = stack[0];
		        cont[key || cont.length] = -(-(tok));
		        key = void 0;
		        break;
		      case 0x22:  // '"'
		        tok = tok.substring(1, tok.length - 1);
		        if (tok.indexOf(SLASH) !== -1) {
		          tok = tok.replace(escapeSequence, unescapeOne);
		        }
		        cont = stack[0];
		        if (!key) {
		          if (cont instanceof Array) {
		            key = cont.length;
		          } else {
		            key = tok || EMPTY_STRING;  // Use as key for next value seen.
		            break;
		          }
		        }
		        cont[key] = tok;
		        key = void 0;
		        break;
		      case 0x5b:  // '['
		        cont = stack[0];
		        stack.unshift(cont[key || cont.length] = []);
		        key = void 0;
		        break;
		      case 0x5d:  // ']'
		        stack.shift();
		        break;
		      case 0x66:  // 'f'
		        cont = stack[0];
		        cont[key || cont.length] = false;
		        key = void 0;
		        break;
		      case 0x6e:  // 'n'
		        cont = stack[0];
		        cont[key || cont.length] = null;
		        key = void 0;
		        break;
		      case 0x74:  // 't'
		        cont = stack[0];
		        cont[key || cont.length] = true;
		        key = void 0;
		        break;
		      case 0x7b:  // '{'
		        cont = stack[0];
		        stack.unshift(cont[key || cont.length] = {});
		        key = void 0;
		        break;
		      case 0x7d:  // '}'
		        stack.shift();
		        break;
		    }
		  }
		  // Fail if we've got an uncompleted object.
		  if (stack.length) { throw new Error(); }
		  return result;
		};
	}-*/;

	public static native JavaScriptObject decode(JavaScriptObject decoder, String json) /*-{
		return decoder(json);
	}-*/;
}
